﻿// Silverlight Vista Sidebar Gadget Template created by Ioan Lazarciuc
// http://www.lazarciuc.ro/ioan
// Contact form present on website
// Based on the Vista Sidebar Gadget Template created by Tim Heuer 
// http://timheuer.com/blog/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Browser;
using SilverlightGadgetUtilities;
using System.Net;
using System.IO;
using System.Windows.Threading;
using Visifire.Charts;
using Visifire.Commons;
using System.Xml.Linq;
using System.Text;
using System.Linq;
using SL4PopupMenu;

namespace WordPressStatsDocked
{
    public partial class Page : UserControl
    {
        SilverlightGadgetEvents evHelper;
        private DispatcherTimer _timerUpdateStats = new DispatcherTimer();

        private Chart _chart;
        private PopupMenu _popupMenu;
        private bool _reloadAllData = false;
        private DateTime _lastUpdate = DateTime.Now;
        private Random _random = new Random(DateTime.Now.Millisecond);

        private const string _dashboard = "/wp-admin/";
        private const string _newPostUrl = "/wp-admin/post-new.php";
        private const string _statsUrl = "/wp-admin/index.php?page=stats";
        private const string _manageComments = "/wp-admin/edit-comments.php";

        private string _errorMsg = "An error has occurred. Sorry for the inconvenience. ({0})";
        private string _errorTitle = "Error";

        private string _noInternetErrorMsg = "It appears that you do not have an active internet connection.  Please connect to the internet in order to retrieve WordPress Statistics.";
        private string _noInternetErrorTitle = "No Internet Connection";

        private int _noInternetConnectionErrorCount = 0;
        
        // Used to hide the Silverlight control from JavaScript when docking/undocking
        [ScriptableMember]
        public bool IsVisible
        {
            get { return this.Visibility == System.Windows.Visibility.Visible; }
            set { this.Visibility = value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; }
        }

        private bool ReloadAllData
        {
            get { return (_reloadAllData || _lastUpdate.Date != DateTime.Today || _chart.Series.Count == 0); }
            set { this._reloadAllData = value; }
        }

        public Page()
        {
            InitializeComponent();
            InitializeChart();
            InitializePopupMenu();
            
            _timerUpdateStats.Tick += new EventHandler(_timerUpdateStats_Tick);

            evHelper = new SilverlightGadgetEvents("SilverlightGadgetEventsD");
            evHelper.VisibilityChanged += new EventHandler(evHelper_VisibilityChanged);
            evHelper.SettingsClosed += new EventHandler<SettingsClosingEventArgs>(evHelper_SettingsClosed);
            evHelper.SettingsClosing += new EventHandler<SettingsClosingEventArgs>(evHelper_SettingsClosing);
            evHelper.ShowSettings += new EventHandler(evHelper_ShowSettings);
            evHelper.Dock += new EventHandler(evHelper_Dock);

            // Set gadget size according to the one specified in the Silverlight control.
            if (SilverlightGadget.Docked)
                SilverlightGadget.SetGadgetSize(Width, Height);            

            HtmlPage.RegisterScriptableObject("dockedGadget", this);
            
            // Not needed if the pages are set in the javascript code
            //SilverlightGadget.FlyoutPage = "flyout.html";
            //SilverlightGadget.SettingsUI = "Settings.html";

            // Set default settings
            if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.UpdateFrequency].Equals(string.Empty))
                SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.UpdateFrequency] = "60";

            if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Period].Equals(string.Empty))
                SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Period] = "Day";

            if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Days].Equals(string.Empty))
                SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Days] = "30";

            if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.ChartType].Equals(string.Empty))
                SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.ChartType] = "Column";

            if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.IncludePublishedPosts].Equals(string.Empty))
                SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.IncludePublishedPosts] = bool.FalseString;


#if DEBUG
            SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri] = "http://nickstips.wordpress.com";
            SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogAPIKey] = "f33553802741";
#endif

            // If we are loading from a reboot, settings will already
            // be set, try and get the statistics and start the timer
            if (SilverlightGadget.Settings.AreValid())
            {
                GetStatistics();
                StartTimer();
            }           
        }

        private void InitializeChart()
        {
            Color borderColor = Color.FromArgb(255, 187, 187, 187);

            _chart = new Chart();
            _chart.Width = 242;
            _chart.Height = 175;
            _chart.ScrollingEnabled = false;
            _chart.CornerRadius = new CornerRadius(6);
            _chart.BorderBrush = new SolidColorBrush(borderColor);
            _chart.BorderThickness = new Thickness(1);

            Title title = new Title();
            title.Text = "Blog Views";            
            title.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;            
            _chart.Titles.Add(title);

            Visifire.Charts.ToolTip toolTip = new Visifire.Charts.ToolTip();
            toolTip.Background = new SolidColorBrush(Colors.White);
            toolTip.CornerRadius = new CornerRadius(0);                        
            toolTip.BorderBrush = new SolidColorBrush(borderColor);
            toolTip.BorderThickness = new Thickness(1);
            toolTip.FontSize = 10;            
            _chart.ToolTips.Add(toolTip);
           
            this.stkPanelMain.Children.Insert(0, _chart);
        }

        #region Popup Menu

        private void InitializePopupMenu()
        {
            _popupMenu = new PopupMenu();            
            _popupMenu.Background = new SolidColorBrush(Color.FromArgb(0, 140, 159, 203));            
            _popupMenu.AddItem("Blog Dashboard", btnBlogDashboard_MouseLeftButtonClick);
            _popupMenu.AddItem("New Post", btnNewPost_MouseLeftButtonClick);
            _popupMenu.AddItem("Site Stats", btnBlogStatistics_MouseLeftButtonClick);
            _popupMenu.AddItem("Manage Comments", btnManageComments_MouseLeftButtonClick);
            _popupMenu.AddItem("Read Blog", btnReadBlog_MouseLeftButtonClick);            
        }

        private void btnArrow_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Display the popup menu
            _popupMenu.Open(new Point(-40, -40), false, 0, btnArrow, e);            
        }

        private void btnBlogDashboard_MouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            HtmlPage.Window.Navigate(new Uri(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri] + _dashboard), "__blank");
        }

        private void btnNewPost_MouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            HtmlPage.Window.Navigate(new Uri(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri] + _newPostUrl), "__blank");
        }

        private void btnBlogStatistics_MouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            HtmlPage.Window.Navigate(new Uri(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri] + _statsUrl), "__blank");
        }

        private void btnManageComments_MouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            HtmlPage.Window.Navigate(new Uri(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri] + _manageComments), "__blank");
        }

        private void btnReadBlog_MouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            HtmlPage.Window.Navigate(new Uri(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri]), "__blank");
        }

        #endregion

        void _timerUpdateStats_Tick(object sender, EventArgs e)
        {
            ReloadAllData = false;
            GetStatistics();
        }

        private void GetStatistics()
        {
            try
            {
                // Check to ensure the settings are valid
                if (SilverlightGadget.Settings.AreValid())
                {
                    // Check to see whether or not to load all data or just the current
                    // day.  There is no reason to keep reloading data from previous 
                    // days, weeks, or months as they will never change.  Further, the more days
                    // requested results in a larger TTL cached period on the stats.wordpress.com site.
                    // According to wordpress.com support staff, "The TTL grows by two minutes per queried day."
                    // Thus requesting 30 days every 10 minutes will result in always retreiving the cached
                    // data as each request will be cached for 60 minutes. After the initial retrieval of data
                    // only request today's views so the TTL for the cached data will only be two minutes.
                    string days = "1";
                    if (ReloadAllData)
                        days = SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Days];

                    StringBuilder uri = new StringBuilder(string.Format("http://stats.wordpress.com/csv.php?api_key={0}&blog_uri={1}&days={2}&end={3}&period={4}",
                                                                        SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogAPIKey],
                                                                        SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.BlogUri],
                                                                        days,
                                                                        DateTime.Today.ToString("yyyy-MM-dd"),
                                                                        SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Period].ToLower()));

                    // We are requesting the same url over and over and one of the "features" of the Silverlight
                    // WebClient class is that it caches data based on uri.  Add a random number query parameter
                    // to ensure a unique uri and thus avoiding any cached values.  It is a hack but apparently
                    // there is no real solution to this yet.
                    uri.Append("&random=" + _random.Next());

                    // Create a WebClient object to retreive the blog views
                    WebClient client = new WebClient();
                    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
                    client.DownloadStringAsync(new Uri(uri.ToString()));
                }
                else
                    this.lblLastUpdated.Content = "Configuration required";
            }            
            catch (Exception ex)
            {
                MessageBox.Show(string.Format(_errorMsg, ex.GetType().ToString()), _errorTitle, MessageBoxButton.OK);
                this.lblLastUpdated.Content = "Error!";
            }
        }
     
        void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            try
            {
                // If we are reloading all data then we will be rebuilding the entire series.
                if (ReloadAllData)
                    _chart.Series.Clear();

                // If there is a problem with the request, the first word of the response will be 'Error'
                if (e.Result.StartsWith("Error"))
                {
                    // Clear the data as there was an error
                    _chart.Series.Clear();

                    // Extract the first line of the error response as that tells the user
                    // what was wrong
                    string message = e.Result;
                    int indexOfNewLine = e.Result.IndexOf('\n');
                    if (indexOfNewLine != -1)
                        message = e.Result.Substring(0, indexOfNewLine);

                    // Let the user know the error that resulted from the query
#if DEBUG
                    MessageBox.Show(e.Result);
#endif
                    MessageBox.Show(message, "Error", MessageBoxButton.OK);
                    this.lblLastUpdated.Content = "Configuration required";
                    return;
                }

                DataSeries dataSeries = new DataSeries();
                dataSeries.RenderAs = RenderAs.Column;

                if(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.ChartType].Equals("Line"))
                    dataSeries.RenderAs = RenderAs.Line;

                // Split the response by a new line character
                string[] days = e.Result.Replace("\"", string.Empty).Split('\n');

                // Go through each line except the first and extract the date and number of views
                for (int i = 1; i < days.Length; i++)
                {
                    // data[0] = date
                    // data[1] = number of views
                    // data[n] = if period is day then posts created on the given date
                    string[] data = days[i].Split(',');

                    if (data.Length >= 2)
                    {
                        // Format the date according to the period that was requested
                        string strDate;
                        string tooltipTitle;
                        DateTime date;
                        if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Period].Equals("Day"))
                        {
                            date = DateTime.Parse(data[0]);                                                       
                            strDate = date.ToString("M/dd");
                            tooltipTitle = date.ToString("dddd, MMMM dd, yyyy");

                            // Error on WordPress stats service.  If you specify the period (which we need to in order
                            // to get the published posts) and the UTC date is tomorrow compared to local date time
                            // the results return tomorrow's result as a 0.  If the currently parsed date
                            // is greater than the current date, end the parsing.
                            if (date > DateTime.Today)
                                break;
                        }
                        else if (SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.Period].Equals("Month"))
                        {
                            date = DateTime.Parse(data[0] + "-01");
                            strDate = date.ToString("MMM \"'\"yy");
                            tooltipTitle = date.ToString("MMMM yyyy");                            
                        }
                        else
                        {
                            strDate = data[0].Substring(5, 2);
                            tooltipTitle = "Week " + strDate;
                        }                        

                        int view;

                        // If parsing is successful at the point to the blogViews collection
                        if (int.TryParse(data[1], out view))
                        {
                            
                            Color color = Color.FromArgb(255, 98, 140, 179);
                            DataPoint dataPoint = new DataPoint();
                            
                            dataPoint.MarkerBorderColor = new SolidColorBrush(color);
                            dataPoint.MarkerColor = new SolidColorBrush(color);
                            dataPoint.MarkerType = MarkerTypes.Circle;                            
                            dataPoint.Color = new SolidColorBrush(color);
                            dataPoint.AxisXLabel = strDate;
                            dataPoint.YValue = view;

                            string tooltipText =  tooltipTitle + Environment.NewLine +  "Views: " + view;

                            // If desired, get any posts that were published on the given date and add it to the tooltip
                            if (bool.Parse(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.IncludePublishedPosts]))
                            {
                                StringBuilder posts = new StringBuilder();
                                int index = 2;
                                while (index < data.Length)
                                    posts.Append(data[index++] + "\r");

                                tooltipText += (posts.Length > 0 ?
                                                                   "\rPublished:\r" +
                                                                   posts.ToString().TrimEnd('\r')
                                                                 : string.Empty);
                            }

                            dataPoint.ToolTipText = tooltipText;

                            // If we are reloading all data then add the dataPoint to the series
                            // otherwise only today's data was requested; just update the last 
                            // point in the existing series
                            if (ReloadAllData)
                                dataSeries.DataPoints.Add(dataPoint);
                            else
                            {
                                _chart.Series[0].DataPoints[_chart.Series[0].DataPoints.Count - 1].YValue = view;
                                _chart.Series[0].DataPoints[_chart.Series[0].DataPoints.Count - 1].ToolTipText = tooltipText;                                
                            }
                        }
                    }
                }

                // Bind the data to the chart
                if (ReloadAllData)
                    _chart.Series.Add(dataSeries);

                // Add events for changing the color of the data points
                _chart.Series[0].MouseEnter += new EventHandler<MouseEventArgs>(ChartSeries_MouseEnter);
                _chart.Series[0].MouseLeave += new EventHandler<MouseEventArgs>(ChartSeries_MouseLeave);               

                // Let the user know the last time the data was updated
                this.lblLastUpdated.Content = "Last updated at " + DateTime.Now.ToLongTimeString();
                this._lastUpdate = DateTime.Now;

                // Reset the no internet connection error count as we had a successful update
                _noInternetConnectionErrorCount = 0;
            }
            // Thrown when the user doesn't have an internet connection
            catch (System.Reflection.TargetInvocationException ex)
            {
                _noInternetConnectionErrorCount++;

                // Don't display a message if this is the first time this exception is being thrown.  A lot of times
                // when a computer is brought back from hibernation or sleep, the gadget will try to get statistics
                // before an internet connection is established.  Give a one time connection error grace period.
                if (_noInternetConnectionErrorCount > 1)
                {
                    MessageBox.Show(_noInternetErrorMsg, _noInternetErrorTitle, MessageBoxButton.OK);
                    _noInternetConnectionErrorCount = 0;
                }

            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format(_errorMsg, ex.GetType().ToString()), _errorTitle, MessageBoxButton.OK);
                this.lblLastUpdated.Content = "Error!";
            }
        }

        void ChartSeries_MouseLeave(object sender, MouseEventArgs e)
        {
            // Change the data point back to the blue color
            DataPoint dataPoint = sender as DataPoint;

            if (dataPoint != null)
            {
                Color color = Color.FromArgb(255, 98, 140, 179);
                dataPoint.Color = new SolidColorBrush(color);
            }
        }       

        void ChartSeries_MouseEnter(object sender, MouseEventArgs e)
        {
            // Change the data point to a yellow color
            DataPoint dataPoint = sender as DataPoint;

            if (dataPoint != null)
            {
                Color color = Color.FromArgb(255, 255, 174, 0);
                dataPoint.Color = new SolidColorBrush(color);
            }
        }        

        void evHelper_Dock(object sender, EventArgs e)
        {
            SilverlightGadget.SetGadgetSize(Width, Height);
        }

        public void evHelper_ShowSettings(object sender, EventArgs e)
        {
            // Stop the timer event while the settings are being changed
            _timerUpdateStats.Stop();   
        }

        void evHelper_SettingsClosing(object sender, SettingsClosingEventArgs e)
        {
        }

        void evHelper_SettingsClosed(object sender, SettingsClosingEventArgs e)
        {

            StartTimer();

            // If the settings were saved then realod the statistics
            if (e.Action == CloseAction.Commit)
            {               
                ReloadAllData = true;
                GetStatistics();
            }
        }

        private void StartTimer()
        {
            if (SilverlightGadget.Settings.AreValid())
            {
                // Update the timer interval
                _timerUpdateStats.Interval = new TimeSpan(0, int.Parse(SilverlightGadget.Settings[SilverlightGadget.SettingsKeys.UpdateFrequency]), 0);                
                _timerUpdateStats.Start();
            }
        }

        void evHelper_VisibilityChanged(object sender, EventArgs e)
        {
        }

        private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // If through code the size of the Silverlight control is changed, change the size in the javascript gadget also.
            if (SilverlightGadget.Docked)
                SilverlightGadget.SetGadgetSize(e.NewSize.Width, e.NewSize.Height);
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // One can save parameters that need to be passed to the flyout control here
            // The parameters can be save in the gadget settings
            //SilverlightGadget.Settings["temppar"] = value;

            // Open the flyout page
            //SilverlightGadget.Flyout();
        }

        private void btnRefresh_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Reload all data if there is not series present
            ReloadAllData = false;
            GetStatistics();
        }       
    }
}
